| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- 'use client';
- import './style.scss';
- import Link from 'next/link';
- import { useRouter } from 'next/navigation';
- import { useState, useEffect, useCallback, useRef, FormEvent } from 'react';
- import Loading from '@/app/component/Loading';
- import { BoardLayout, PostConst } from '@/constants/forum';
- import { fetchBoard } from '@/lib/api/forum/board';
- import { fetchPostUpdate } from '@/lib/api/forum/post';
- import { throwError } from '@/lib/utils/client';
- import BoardResponse from '@/dtos/response/forum/board/boardResponse';
- import BoardListResponse from '@/dtos/response/forum/board/boardListResponse';
- import PostResponse from '@/dtos/response/forum/post/postResponse';
- import Editor, { Handle } from '../../_component/Editor';
- import HeaderContent from '../../_component/HeaderContent';
- import FooterContent from '../../_component/FooterContent';
- import PostTagInput from '../../_component/PostTagInput';
- type Props = {
- _boardList: BoardListResponse[],
- _board: BoardResponse,
- _post: PostResponse
- };
- export default function View({ _boardList, _board, _post }: Props)
- {
- const router = useRouter();
- const editorRef = useRef<Handle>(null);
- const [error, setError] = useState<string|null>(null);
- const [loading, setLoading] = useState<boolean>(false);
- const [isChanged, setIsChanged] = useState<boolean>(false);
- const [board, setBoard] = useState<BoardResponse|null>(_board);
- const [boardCode, setBoardCode] = useState<string>(_post.boardCode);
- const [boardPrefixID, setBoardPrefixID] = useState<string>(_post.boardPrefixID?.toString() ?? '');
- const [subject, setSubject] = useState<string>(_post.subject);
- const [content, setContent] = useState<string>(_post.content);
- const [isSecret, setIsSecret] = useState<boolean>(_post.isSecret);
- const [isNotice, setIsNotice] = useState<boolean>(_post.isNotice);
- const [isSpeaker, setIsSpeaker] = useState<boolean>(_post.isSpeaker);
- const [tags, setTags] = useState<string[]>(_post.tagList.map((tag) => tag.slug));
- const boardCodeRef = useRef<HTMLSelectElement>(null);
- const boardPrefixIDRef = useRef<HTMLSelectElement>(null);
- const subjectRef = useRef<HTMLInputElement>(null);
- const contentRef = useRef<HTMLTextAreaElement>(null);
- const redirectUrl = `/post/${_post.id}`;
- useEffect(() => {
- if (error) {
- alert(error);
- setError(null);
- }
- }, [error]);
- // 게시글 초기화
- const resetForm = () => {
- setError('');
- setIsChanged(false);
- setBoardPrefixID('');
- setSubject(board?.boardMeta.write.defaultSubject || '');
- setContent(board?.boardMeta.write.defaultContent || '');
- setIsSecret(false);
- setIsNotice(false);
- setIsSpeaker(false);
- setTags([]);
- // Editor 초기화
- if (editorRef.current?.editorInstance) {
- editorRef.current.editorInstance.setData(content);
- }
- };
- // 게시판 선택 시
- const handleBoardChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
- const code = e.target.value;
- if (code) {
- return;
- }
- if (isChanged) {
- if (!confirm('작성 중인 내용이 사라질 수 있습니다. 게시판을 변경하시겠습니까?')) {
- return
- }
- }
- setLoading(true);
- fetchBoard(boardCode).then((res) => {
- if (res.ok) {
- setBoardCode(code);
- setBoard(res.data);
- setIsChanged(false);
- resetForm();
- } else {
- throw new Error('게시판을 조회할 수 없습니다.');
- }
- }).catch((err) => {
- setError(err.message);
- }).finally(() => {
- setLoading(false);
- });
- };
- // 제목, 내용, 말머리 변경 시
- const handleChange = (e: React.ChangeEvent<HTMLSelectElement|HTMLInputElement|HTMLTextAreaElement>) => {
- const { name, value } = e.target as HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement;
- const checked = (e.target as HTMLInputElement).checked;
- switch (name) {
- case 'boardPrefixID':
- setBoardPrefixID(value);
- break;
- case 'isSecret':
- setIsSecret(checked);
- break;
- case 'isNotice':
- setIsNotice(checked);
- setIsSpeaker(false);
- break;
- case 'isSpeaker':
- setIsSpeaker(checked);
- setIsNotice(false);
- break;
- case 'subject':
- setSubject(value);
- break;
- case 'content':
- setContent(value);
- break;
- }
- setIsChanged(true);
- };
- // CKEditor에서 내용 변경 시
- const handleEditorChange = useCallback((data: string) => {
- setContent(data);
- setIsChanged(true);
- }, []);
- const validate = () => {
- if (!boardCode || !board) {
- boardCodeRef.current!.focus();
- throw new Error('게시판을 선택해주세요.');
- }
- if (board.boardMeta.write.allowPrefix && board.boardMeta.write.requiredPrefix && !boardPrefixID) {
- boardPrefixIDRef.current!.focus();
- throw new Error((board.boardMeta.list.layout === BoardLayout.QnA ? '분류' : '말머리') + '를 선택해주세요.');
- }
- if (!subject) {
- subjectRef.current!.focus();
- throw new Error('제목을 입력해주세요.');
- } else if (subject.length > PostConst.maxAllowedSubjectLength) {
- subjectRef.current!.focus();
- throw new Error(`제목은 ${PostConst.maxAllowedSubjectLength}자 이내로 작성해주세요.`);
- }
- if (!content) {
- if (board.boardMeta.write.allowEditor) {
- editorRef.current!.editorInstance?.editing.view.focus();
- } else {
- contentRef.current!.focus();
- }
- throw new Error('내용을 입력해주세요.');
- } else if (!board.boardMeta.write.allowEditor) {
- // 기본 textarea 사용 시 글자 수 검사
- if (content.length > PostConst.maxAllowedContentLength) {
- contentRef.current!.focus();
- throw new Error(`내용은 ${PostConst.maxAllowedContentLength}자 이내로 작성해주세요.`);
- }
- }
- if (board.boardMeta.write.allowTag && tags.length > board.boardMeta.write.tagLimit) {
- throw new Error(`태그는 ${board.boardMeta.write.tagLimit}개 이내로 작성해주세요.`);
- }
- };
- // 게시글 등록 처리
- const handleSubmit = useCallback(async (e: FormEvent) => {
- e.preventDefault();
- try {
- validate();
- setLoading(true);
- if (!board) {
- throw new Error('게시판을 선택해 주세요.');
- }
- const formData = new FormData();
- formData.append('postID', _post.id.toString());
- formData.append('boardID', board.id.toString());
- formData.append('boardCode', boardCode);
- formData.append('boardPrefixID', boardPrefixID);
- formData.append('isSecret', isSecret.toString());
- formData.append('isNotice', isNotice.toString());
- formData.append('isSpeaker', isSpeaker.toString());
- formData.append('subject', subject);
- if (content) {
- const doc = new DOMParser().parseFromString(content, 'text/html');
- doc.querySelectorAll('img[src]').forEach(img => {
- const src = img.getAttribute('src');
- if (src && src.startsWith('data:image/')) {
- img.setAttribute('src', 'data:image/');
- }
- });
- formData.append('content', doc.body.innerHTML);
- }
- // 태그
- if (board.boardMeta.write.allowTag) {
- tags.forEach(tag => formData.append('tags', tag));
- }
- // 이미지 정보
- if (board.boardMeta.write.allowEditor && board.boardMeta.write.allowImage) {
- editorRef.current?.getImageStore().forEach(i => {
- if (i.image?.size > 0 && i.name) {
- formData.append('images', i.image, i.name);
- }
- });
- }
- // 미디어 정보
- if (board.boardMeta.write.allowEditor && board.boardMeta.write.allowMedia) {
- editorRef.current!.getMediaStore().forEach((m) => {
- if (m.url) {
- formData.append('medias', m.url);
- }
- });
- }
- // 첨부 파일
- if (board.boardMeta.write.allowEditor && board.boardMeta.write.allowFile) {
- editorRef.current!.getFileStore().forEach(f => {
- if (f?.size > 0 && f.name) {
- formData.append('files', f.file, f.name);
- }
- });
- }
- const res = await fetchPostUpdate(formData);
- if (res.ok) {
- resetForm();
- router.push(redirectUrl);
- } else {
- throwError(res);
- }
- } catch (err: any) {
- setError(err.message);
- } finally {
- setLoading(false);
- }
- }, [boardCode, board, boardPrefixID, subject, content, isSecret, isNotice, isSpeaker, tags]);
- return (
- <form id='postEdit' onSubmit={handleSubmit}>
- {loading && <Loading />}
- <fieldset>
- <legend><h1>{board?.name} 글 수정</h1></legend>
- {/* 상단 안내 */}
- {<HeaderContent isEnabled={board?.boardMeta.write.showHeader} content={board?.boardMeta.write.headerContent}/>}
- {/* 게시판 선택, 말머리, 비밀글, 공지, 전체 공지 */}
- <section>
- {/* 게시판 선택 */}
- <article>
- <select name='boardCode' ref={boardCodeRef} value={boardCode} onChange={handleBoardChange} title='게시판 선택'>
- {_boardList.map((board) => (
- <option key={board.code} value={board.code}>{board.name}</option>
- ))}
- </select>
- </article>
- {/* 말머리 */}
- {board?.boardMeta.write.allowPrefix && (
- <article>
- <select name='boardPrefixID' ref={boardPrefixIDRef} value={boardPrefixID} onChange={handleChange} title='말머리 선택'>
- <option value=''>{(board.boardMeta.list.layout === BoardLayout.QnA ? '분류' : '말머리') + ' 선택'}</option>
- {board.boardPrefix.map((row) => (
- <option key={row.id} value={row.id}>{row.name}</option>
- ))}
- </select>
- </article>
- )}
- <article>
- {/* 비밀글 */}
- {board?.boardMeta.write.allowSecret && (
- <>
- <input type='checkbox' name='isSecret' id='isSecret' checked={isSecret} onChange={handleChange} />
- <label htmlFor='isSecret'>비밀글</label>
- </>
- )}
- {/* 해당 게시판 공지 */}
- <input type='checkbox' name='isNotice' id='isNotice' checked={isNotice} onChange={handleChange} />
- <label htmlFor='isNotice'>공지</label>
- {/* 게시판 전체 공지 */}
- <input type='checkbox' name='isSpeaker' id='isSpeaker' checked={isSpeaker} onChange={handleChange} />
- <label htmlFor='isSpeaker'>전체 공지</label>
- </article>
- </section>
- {/* 제목 */}
- <section>
- <input type='text' name='subject' ref={subjectRef} value={subject} onChange={handleChange} placeholder='글 제목을 입력해주세요.' autoFocus maxLength={PostConst.maxAllowedSubjectLength} />
- </section>
- {/* 내용 */}
- <section>
- {board?.boardMeta.write.allowEditor ?
- (
- <Editor ref={editorRef} key={boardCode} data={content} onChange={handleEditorChange} boardMeta={board?.boardMeta} />
- ) : (
- <textarea name='content' ref={contentRef} value={content} onChange={handleChange} placeholder='내용을 입력해주세요.' maxLength={PostConst.maxAllowedContentLength}></textarea>
- )}
- </section>
- {/* 태그 */}
- {board?.boardMeta.write.allowTag && (
- <section id='postTag'>
- <PostTagInput value={tags} onChange={setTags} maxTags={board.boardMeta.write.tagLimit} />
- </section>
- )}
- {/* 하단 안내 */}
- {<FooterContent isEnabled={board?.boardMeta.write.showFooter} content={board?.boardMeta.write.footerContent}/>}
- <br/>
- <section>
- <button type='submit' className='btn btn-submit' disabled={loading}>
- { loading ? '수정 중…' : '확인' }
- </button>
- <Link href={redirectUrl} className='btn btn-default'>취소</Link>
- </section>
- </fieldset>
- </form>
- );
- }
|